<?php

namespace plugin\admin\app\service\v1;

use plugin\admin\app\common\fast\Random;
use plugin\admin\app\common\Util;
use plugin\admin\app\constant\v1\CommonConstant;
use plugin\admin\app\model\V1Order;
use plugin\admin\app\model\V1OrderNode;
use support\exception\BusinessException;
use Webman\RedisQueue\Redis;

/**
 * 订单节点服务类
 **/
class OrderNodeService
{

    public static function node_field()
    {
        return ['id', 'user_id', 'order_id', 'line_id', 'game_id', 'duration', 'amount', 'expire_time', 'source', 'status', 'region_id', 'server_id', 'code', 'ip', 'port', 'ip_ems', 'account', 'pass', 'data', 'refund_status', 'created_at'];
    }

    /**
     * 续费时间列表
     *
     * @param integer $lineId
     * @param array $admin 管理员/会员
     * @param array $user 会员
     */
    public static function getNodeRenewDurationList($lineId, $admin, $user)
    {
        list($line_list, $line_column) = LineService::select(['*'], ['id' => $lineId]);
        $userExtend = UserExtendService::getUserExtend($line_column, $user);
        $userExtend = UserExtendService::addNameToExtend($line_column, $userExtend);

        $list = [];
        if (isset($userExtend[0])) {
            $info = $userExtend[0];
            $get_duration_sec_list = CommonConstant::get_duration_sec_list();
            foreach ($get_duration_sec_list as $duration => $name) {
                if ($info[$duration . '_show'] == CommonConstant::DEFAULT_1) {
                    $data = [];
                    $data['duration'] = $duration;
                    $data['name'] = $name;
                    $data['money'] = $info[$duration];
                    $list[] = $data;
                }
            }
        }
        return $list;
    }

    /**
     * 续费/自动续费
     *
     * @param array $params
     * @param array $admin 管理员/会员
     * @param array $user 会员
     * @param string $function
     */
    public static function renew($params, $admin, $user, $function = '')
    {
        list($lineId, $duration, $ids) = array_values($params);

        $db = Util::db();

        // 用户权限和价格
        $amount = self::renewGetUserExtendInfo($db, $lineId, $duration, $user);

        // 订单节点数据
        $orderNodeList = self::renewGetNodeList($lineId, $ids, $user, $function);

        // 计算时间阈值
        $hourAgo = OptionService::getConfigReserve();

        // 处理订单节点数据
        list($orderData, $updateOrderNodeData, $logData, $apiDataDel, $apiDataAdd) = self::renewProcessNodeList($lineId, $duration, $admin, $user, $amount, $orderNodeList, $hourAgo);

        // 执行操作
        $moneyLogId = self::renewExecute($db, $lineId, $orderData, $updateOrderNodeData, $logData);

        // 队列名
        // 数据，可以直接传数组，无需序列化
        // 投递消息
        Redis::send('commission', ['commission_type' => CommonConstant::DEFAULT_1, 'type' => CommonConstant::MONEY_LOG_TYPE_5, 'money_log_id' => $moneyLogId]);

        $apiResDel = Util::batchUserApi('del', $apiDataDel);
        $apiResAdd = Util::batchUserApi('add', $apiDataAdd);
        $apiRes = array_merge($apiResDel, $apiResAdd);
        return V(0, $apiRes ? implode(',', $apiRes) : '续费成功', []);
    }

    /**
     * 续费-用户权限和价格
     **/
    public static function renewGetUserExtendInfo($db, $lineId, $duration, $user)
    {
        $userExtendInfo = $db->table('wa_v1_user_extend')
            ->where('user_id', $user['id'])
            ->where('line_id', $lineId)
            ->select(['id', $duration])
            ->first();
        if (!$userExtendInfo) {
            throw new BusinessException('系统价格配置不存在！');
        }
        $amount = $userExtendInfo->{$duration} ?? 0;
        if ($amount <= 0) {
            throw new BusinessException('当前用户卡价格无效！');
        }
        return $amount;
    }

    /**
     * 续费-订单节点数据
     **/
    public static function renewGetNodeList($lineId, $ids, $user, $function)
    {
        $orderNodeList = V1OrderNode::where('deleted', CommonConstant::DELETE_0)
            ->where('user_id', $user['id'])
            ->where('line_id', $lineId)
            ->whereIn('id', $ids)
            ->select(self::node_field())
            ->with(['server'])
            ->get();
        if ($orderNodeList->isEmpty()) {
            throw new BusinessException('没有订单节点！');
        }

        if (count($orderNodeList) != count($ids)) {
            throw new BusinessException('部分订单节点不存在！');
        }

        if ($function == 'renew_auto_queue') {
            // 提前检查节点状态
            $expiredNodes = $orderNodeList->where('status', CommonConstant::DEFAULT_1);
            if ($expiredNodes->isNotEmpty()) {
                throw new BusinessException('存在到期节点，不能自动续费！');
            }
        }

        // 提前检查节点状态
        $expiredNodes = $orderNodeList->where('status', CommonConstant::DEFAULT_2);
        if ($expiredNodes->isNotEmpty()) {
            throw new BusinessException('存在过期节点，不能续费');
        }

        $refundNodes = $orderNodeList->where('refund_status', CommonConstant::DEFAULT_1);
        if ($refundNodes->isNotEmpty()) {
            throw new BusinessException('存在退款审核中节点，不能续费！');
        }

        $refundNodes = $orderNodeList->where('refund_status', CommonConstant::DEFAULT_2);
        if ($refundNodes->isNotEmpty()) {
            throw new BusinessException('存在退款审核通过节点，不能续费！');
        }
        return $orderNodeList;
    }

    /**
     * 续费-处理订单节点数据
     */
    public static function renewProcessNodeList($lineId, $duration, $admin, $user, $amount, $orderNodeList, $hourAgo)
    {
        // 预计算续费时间
        $durationSec = CommonConstant::get_duration_sec_data()[$duration][1];

        $orderData = [];
        $updateOrderNodeData = [];
        $totalNumber = 0;
        $totalAmount = 0;
        $apiDataDel = [];
        $apiDataAdd = [];
        foreach ($orderNodeList as $orderNode) {
            $expire = strtotime($orderNode->expire_time);
            if ($expire <= $hourAgo) {
                throw new BusinessException('存在过期节点，不能续费！');
            }

            $expireTime = $orderNode->status == CommonConstant::DEFAULT_0 ? $expire + $durationSec : time() + $durationSec;
            $expireDateTime = date('Y-m-d H:i:s', $expireTime);
            $orderId = $orderNode->order_id;
            $nodeId = $orderNode->id;

            // 初始化订单数据
            if (!isset($orderData[$orderId])) {
                $orderData[$orderId] = [
                    'duration' => '',
                    'expire_time' => 0,
                    'status' => CommonConstant::DEFAULT_0,
                ];
            }
            // 跟踪订单的最大过期时间
            if ($expireDateTime > $orderData[$orderId]['expire_time']) {
                $orderData[$orderId]['duration'] = $duration;
                $orderData[$orderId]['expire_time'] = $expireDateTime;
            }

            $orderNodeData = [];
            $orderNodeData['duration'] = $duration;
            $orderNodeData['expire_time'] = $expireDateTime;
            $orderNodeData['status'] = CommonConstant::DEFAULT_0;
            $orderNodeData['amount'] = $amount;
            $updateOrderNodeData[$nodeId] = $orderNodeData;

            $totalNumber++;
            $totalAmount += $amount;

            // 请求接口数据
            if ($orderNode->server) {
                $serverInfo = $orderNode->server;
                $url = 'http://' . $serverInfo->ip . ':' . $serverInfo->port . '/user/';
                $apiData = [];
                $apiData['url'] = $url;
                $apiData['ip'] = $serverInfo->ip;
                $apiData['port'] = $serverInfo->port;
                $apiData['key'] = $serverInfo->key;
                $apiData['username'] = $orderNode->account;
                $apiData['password'] = $orderNode->pass;
                $apiDataDel[] = $apiData;
                $apiData = [];
                $apiData['url'] = $url;
                $apiData['ip'] = $serverInfo->ip;
                $apiData['port'] = $serverInfo->port;
                $apiData['key'] = $serverInfo->key;
                $apiData['username'] = $orderNode->account;
                $apiData['password'] = $orderNode->pass;
                $apiData['user_type'] = $serverInfo->line_type; // 线路类型，1.socks5 2.HTTP -1.通用
                $apiData['limit_up'] = $serverInfo->bandwidth_up;
                $apiData['limit_down'] = $serverInfo->bandwidth_down;
                $apiData['maxcon'] = $serverInfo->link_url_number;
                $apiData['enable'] = true; // 是否启用此用户，用于临时禁用
                $apiData['shadowtime'] = $expireDateTime; // 到期时间 注意要补0
                $apiData['info'] = 1; // 预留参数
                $apiData['bindip'] = $orderNode->ip_ems; // 绑定的内网IP参数，如果为空则不限制
                $apiData['uid'] = $user['username'];
                $apiDataAdd[] = $apiData;
            }
        }

        // 余额校验
        if ($user['money'] < $totalAmount) {
            throw new BusinessException('余额不足！');
        }

        $logData = [
            'admin_id' => $admin['id'],
            'admin_name' => $admin['username'],
            'user_id' => $user['id'],
            'user_name' => $user['username'],
            'type' => CommonConstant::MONEY_LOG_TYPE_5,
            'change_type' => CommonConstant::DEFAULT_20,
            'money_before' => $user['money'],
            'money' => $totalAmount,
            'memo' => "续费{$totalNumber}个节点【" . CommonConstant::get_duration_type_list()[$duration] . "】",
            'line_id' => $lineId,
            'duration' => $duration,
        ];

        return [
            $orderData,
            $updateOrderNodeData,
            $logData,
            $apiDataDel,
            $apiDataAdd,
        ];
    }

    /**
     * 续费-执行操作
     **/
    public static function renewExecute($db, $lineId, $orderData, $updateOrderNodeData, $logData)
    {
        $moneyLogId = $db->transaction(function () use ($db, $lineId, $orderData, $updateOrderNodeData, $logData) {
            $orderList = $db->table('wa_v1_order')
                ->whereIn('id', array_keys($orderData))
                ->select(['id', 'expire_time'])
                ->get()
                ->toArray();
            $orderDataNew = [];
            if ($orderList) {
                foreach ($orderList as $orderInfo) {
                    $orderId = $orderInfo->id;
                    $order = $orderData[$orderId] ?? null;
                    if ($order) {
                        if ($orderInfo->expire_time < $order['expire_time']) {
                            $orderDataNew[$orderId] = $order;
                        }
                    }
                }
            }

            // 批量更新订单
            if ($orderDataNew) {
                $fields = ['duration', 'expire_time', 'status'];
                list($sql, $allBindings) = Util::buildBatchUpdate('wa_v1_order', $fields, $orderDataNew);
                $db->statement($sql, $allBindings);
            }

            // 批量更新订单节点
            $fields = ['duration', 'expire_time', 'status', 'amount'];
            list($sql, $allBindings) = Util::buildBatchUpdate('wa_v1_order_node', $fields, $updateOrderNodeData);
            $db->statement($sql, $allBindings);

            $orderNodeIds = array_keys($updateOrderNodeData);
            $orderNodeIdsStr = implode(',', $orderNodeIds);
            // 余额变动
            $logData['order_node_ids'] = $orderNodeIdsStr;
            $logId = UserService::money($logData, $logData['user_id'], []);

            return $logId;
        });
        return $moneyLogId;
    }


    /**
     * 过户
     *
     * @param array $params
     * @param array $admin 管理员/会员
     * @param array $user 会员
     */
    public static function transfer($params, $admin, $user)
    {
        list($lineId, $toUserId, $ids) = array_values($params);

        $db = Util::db();

        // 过户会员信息
        $userInfo = self::transferGetUserInfo($db, $toUserId, $user);

        // 订单节点数据
        $orderNodeList = self::transferGetNodeList($lineId, $ids, $user);

        // 计算时间阈值
        $hourAgo = OptionService::getConfigReserve();

        // 处理订单节点数据
        list($orderData) = self::transferProcessNodeList($orderNodeList, $hourAgo);

        // 执行操作
        self::transferExecute($db, $lineId, $orderData, $userInfo);

        return true;
    }

    /**
     * 过户-过户会员信息
     **/
    public static function transferGetUserInfo($db, $toUserId, $user)
    {
        $userInfo = $db->table('wa_admins')
            ->where('id', $toUserId)
            ->select(['id', 'username', 'invitation_id', 'ident_type'])
            ->first();
        if (!$userInfo) {
            throw new BusinessException('过户会员不存在或已删除！');
        }
        if ($userInfo->ident_type == 0) {
            throw new BusinessException('过户会员未实名认证，不能过户！');
        }
        if ($userInfo->invitation_id != $user['id']) {
            throw new BusinessException('过户会员不是您的下级！');
        }
        return $userInfo;
    }

    /**
     * 过户-订单节点数据
     **/
    public static function transferGetNodeList($lineId, $ids, $user)
    {
        $orderNodeList = V1OrderNode::where('deleted', CommonConstant::DELETE_0)
            ->where('user_id', $user['id'])
            ->where('line_id', $lineId)
            ->whereIn('id', $ids)
            ->select(self::node_field())
            ->get();
        if ($orderNodeList->isEmpty()) {
            throw new BusinessException('没有订单节点！');
        }

        if (count($orderNodeList) != count($ids)) {
            throw new BusinessException('部分订单节点不存在！');
        }

        // 提前检查节点状态
        $expiredNodes = $orderNodeList->where('status', CommonConstant::DEFAULT_2);
        if ($expiredNodes->isNotEmpty()) {
            throw new BusinessException('存在过期节点，不能过户');
        }

        $refundNodes = $orderNodeList->where('refund_status', CommonConstant::DEFAULT_1);
        if ($refundNodes->isNotEmpty()) {
            throw new BusinessException('存在退款审核中节点，不能过户！');
        }

        $refundNodes = $orderNodeList->where('refund_status', CommonConstant::DEFAULT_2);
        if ($refundNodes->isNotEmpty()) {
            throw new BusinessException('存在退款审核通过节点，不能过户！');
        }
        return $orderNodeList;
    }

    /**
     * 过户-处理订单节点数据
     */
    public static function transferProcessNodeList($orderNodeList, $hourAgo)
    {
        $orderData = [];
        foreach ($orderNodeList as $orderNode) {
            $expire = strtotime($orderNode->expire_time);
            if ($expire <= $hourAgo) {
                throw new BusinessException('存在过期节点，不能过户！');
            }

            $expireDateTime = $orderNode->expire_time;
            $gameId = $orderNode->game_id;

            // 初始化游戏订单数据
            if (!isset($orderData[$gameId])) {
                $orderData[$gameId] = [
                    'nodeIds' => [],
                    'duration' => '',
                    'expireTime' => 0,
                ];
            }

            // 更新游戏订单数据
            $orderData[$gameId]['nodeIds'][] = $orderNode->id;
            if ($expireDateTime > $orderData[$gameId]['expireTime']) {
                $orderData[$gameId]['duration'] = $orderNode->duration;
                $orderData[$gameId]['expireTime'] = $expireDateTime;
            }
        }
        return [$orderData];
    }

    /**
     * 过户-执行操作
     */
    public static function transferExecute($db, $lineId, $orderData, $userInfo)
    {
        $db->transaction(function () use ($db, $lineId, $orderData, $userInfo) {
            foreach ($orderData as $gameId => $order) {
                $orderNo = get_order_sn($userInfo->id);

                // 添加订单
                $data = [];
                $data['user_id'] = $userInfo->id;
                $data['user_name'] = $userInfo->username;
                $data['order_no'] = $orderNo;
                $data['line_id'] = $lineId;
                $data['game_id'] = $gameId;
                $data['duration'] = $order['duration'];
                $data['expire_time'] = $order['expireTime'];
                $data['remark'] = '';
                $data['source'] = CommonConstant::DEFAULT_1;
                $data['created_at'] = date('Y-m-d H:i:s');
                $orderId = $db->table('wa_v1_order')->insertGetId($data);

                // 更新订单节点
                $updateData = [];
                $updateData['user_id'] = $userInfo->id;
                $updateData['user_name'] = $userInfo->username;
                $updateData['order_id'] = $orderId;
                $updateData['order_no'] = $orderNo;
                $updateData['source'] = CommonConstant::DEFAULT_1;
                $db->table('wa_v1_order_node')->whereIn('id', $order['nodeIds'])->update($updateData);
            }
        });
        return true;
    }


    /**
     * 平均时间
     *
     * @param array $params
     * @param array $admin 管理员/会员
     * @param array $user 会员
     */
    public static function averageTime($params, $admin, $user)
    {
        list($lineId, $date, $ids) = array_values($params);

        $db = Util::db();

        $avgData = OptionService::getConfigAvgData();
        if (!$avgData) {
            throw new BusinessException('平均时间配置不存在');
        }

        // 订单节点数据
        $orderNodeList = self::averageTimeGetNodeList($lineId, $ids, $user);

        // 计算时间阈值
        $hourAgo = OptionService::getConfigReserve();

        // 处理订单节点数据
        list($orderData, $updateOrderNodeData, $logData, $apiDataDel, $apiDataAdd) = self::averageTimeProcessNodeList($lineId, $date, $admin, $user, $orderNodeList, $avgData, $hourAgo);

        // 执行操作
        self::averageTimeExecute($db, $lineId, $orderData, $updateOrderNodeData, $logData);

        $apiResDel = Util::batchUserApi('del', $apiDataDel);
        $apiResAdd = Util::batchUserApi('add', $apiDataAdd);
        $apiRes = array_merge($apiResDel, $apiResAdd);
        return V(0, $apiRes ? implode(',', $apiRes) : '平均时间成功', []);
    }

    /**
     * 平均时间-订单节点数据
     **/
    public static function averageTimeGetNodeList($lineId, $ids, $user)
    {
        $orderNodeList = V1OrderNode::where('deleted', CommonConstant::DELETE_0)
            ->where('user_id', $user['id'])
            ->where('line_id', $lineId)
            ->whereIn('id', $ids)
            ->select(self::node_field())
            ->with(['server'])
            ->get();
        if ($orderNodeList->isEmpty()) {
            throw new BusinessException('没有订单节点！');
        }

        if (count($orderNodeList) != count($ids)) {
            throw new BusinessException('部分订单节点不存在！');
        }

        // 提前检查节点状态
        $expiredNodes = $orderNodeList->where('status', CommonConstant::DEFAULT_1);
        if ($expiredNodes->isNotEmpty()) {
            throw new BusinessException('存在到期节点，不能平均时间！');
        }

        // 提前检查节点状态
        $expiredNodes = $orderNodeList->where('status', CommonConstant::DEFAULT_2);
        if ($expiredNodes->isNotEmpty()) {
            throw new BusinessException('存在过期节点，不能平均时间');
        }

        $refundNodes = $orderNodeList->where('refund_status', CommonConstant::DEFAULT_1);
        if ($refundNodes->isNotEmpty()) {
            throw new BusinessException('存在退款审核中节点，不能平均时间！');
        }

        $refundNodes = $orderNodeList->where('refund_status', CommonConstant::DEFAULT_2);
        if ($refundNodes->isNotEmpty()) {
            throw new BusinessException('存在退款审核通过节点，不能平均时间！');
        }
        return $orderNodeList;
    }

    /**
     * 平均时间-处理订单节点数据
     */
    public static function averageTimeProcessNodeList($lineId, $date, $admin, $user, $orderNodeList, $avgData, $hourAgo)
    {
        $datetime = strtotime($date);

        $orderData = [];
        $updateOrderNodeData = [];
        $totalNumber = 0;
        $totalAmount = 0;
        $apiDataDel = [];
        $apiDataAdd = [];
        foreach ($orderNodeList as $orderNode) {
            $expire = strtotime($orderNode->expire_time);
            if ($expire <= $hourAgo) {
                throw new BusinessException('存在过期节点，不能平均时间！');
            }
            if ($datetime <= $expire) {
                throw new BusinessException('平均时间必须大于节点到期时间！');
            }
            $surplus_time = $datetime - $expire;
            $surplus_day = round(bcdiv($surplus_time, 86400, 2));
            if ($surplus_day < 1) {
                throw new BusinessException('平均时间不能小于1天！');
            }
            $amount = OptionService::getAvgAmount($avgData, $surplus_day);
            if ($amount <= 0) {
                throw new BusinessException('平均时间配置不存在！');
            }

            $expireDateTime = $date;
            $orderId = $orderNode->order_id;
            $nodeId = $orderNode->id;

            // 初始化订单数据
            if (!isset($orderData[$orderId])) {
                $orderData[$orderId] = [
                    'expire_time' => $expireDateTime,
                    'status' => CommonConstant::DEFAULT_0,
                ];
            }

            $orderNodeData = [];
            $orderNodeData['expire_time'] = $expireDateTime;
            $orderNodeData['status'] = CommonConstant::DEFAULT_0;
            $orderNodeData['amount'] = $orderNode->amount + $amount;
            $updateOrderNodeData[$nodeId] = $orderNodeData;

            $totalNumber++;
            $totalAmount += $amount;

            // 请求接口数据
            if ($orderNode->server) {
                $serverInfo = $orderNode->server;
                $url = 'http://' . $serverInfo->ip . ':' . $serverInfo->port . '/user/';
                $apiData = [];
                $apiData['url'] = $url;
                $apiData['ip'] = $serverInfo->ip;
                $apiData['port'] = $serverInfo->port;
                $apiData['key'] = $serverInfo->key;
                $apiData['username'] = $orderNode->account;
                $apiData['password'] = $orderNode->pass;
                $apiDataDel[] = $apiData;
                $apiData = [];
                $apiData['url'] = $url;
                $apiData['ip'] = $serverInfo->ip;
                $apiData['port'] = $serverInfo->port;
                $apiData['key'] = $serverInfo->key;
                $apiData['username'] = $orderNode->account;
                $apiData['password'] = $orderNode->pass;
                $apiData['user_type'] = $serverInfo->line_type; // 线路类型，1.socks5 2.HTTP -1.通用
                $apiData['limit_up'] = $serverInfo->bandwidth_up;
                $apiData['limit_down'] = $serverInfo->bandwidth_down;
                $apiData['maxcon'] = $serverInfo->link_url_number;
                $apiData['enable'] = true; // 是否启用此用户，用于临时禁用
                $apiData['shadowtime'] = $expireDateTime; // 到期时间 注意要补0
                $apiData['info'] = 1; // 预留参数
                $apiData['bindip'] = $orderNode->ip_ems; // 绑定的内网IP参数，如果为空则不限制
                $apiData['uid'] = $user['username'];
                $apiDataAdd[] = $apiData;
            }
        }

        // 余额校验
        if ($user['money'] < $totalAmount) {
            throw new BusinessException('余额不足！');
        }

        $logData = [
            'admin_id' => $admin['id'],
            'admin_name' => $admin['username'],
            'user_id' => $user['id'],
            'user_name' => $user['username'],
            'type' => CommonConstant::MONEY_LOG_TYPE_6,
            'change_type' => CommonConstant::DEFAULT_20,
            'money_before' => $user['money'],
            'money' => $totalAmount,
            'memo' => "平均时间{$totalNumber}个节点",
            'line_id' => $lineId,
            'duration' => '',
        ];

        return [
            $orderData,
            $updateOrderNodeData,
            $logData,
            $apiDataDel,
            $apiDataAdd,
        ];
    }

    /**
     * 平均时间-执行操作
     **/
    public static function averageTimeExecute($db, $lineId, $orderData, $updateOrderNodeData, $logData)
    {
        $moneyLogId = $db->transaction(function () use ($db, $lineId, $orderData, $updateOrderNodeData, $logData) {
            $orderList = $db->table('wa_v1_order')
                ->whereIn('id', array_keys($orderData))
                ->select(['id', 'expire_time'])
                ->get()
                ->toArray();
            $orderDataNew = [];
            if ($orderList) {
                foreach ($orderList as $orderInfo) {
                    $orderId = $orderInfo->id;
                    $order = $orderData[$orderId] ?? null;
                    if ($order) {
                        if ($orderInfo->expire_time < $order['expire_time']) {
                            $orderDataNew[$orderId] = $order;
                        }
                    }
                }
            }

            // 批量更新订单
            if ($orderDataNew) {
                $fields = ['expire_time', 'status'];
                list($sql, $allBindings) = Util::buildBatchUpdate('wa_v1_order', $fields, $orderDataNew);
                $db->statement($sql, $allBindings);
            }

            // 批量更新订单节点
            $fields = ['expire_time', 'status', 'amount'];
            list($sql, $allBindings) = Util::buildBatchUpdate('wa_v1_order_node', $fields, $updateOrderNodeData);
            $db->statement($sql, $allBindings);

            $orderNodeIds = array_keys($updateOrderNodeData);
            $orderNodeIdsStr = implode(',', $orderNodeIds);
            // 余额变动
            $logData['order_node_ids'] = $orderNodeIdsStr;
            $logId = UserService::money($logData, $logData['user_id'], []);

            return $logId;
        });
        return $moneyLogId;
    }


    /**
     * 修改密码
     *
     * @param array $params
     * @param array $admin 管理员/会员
     * @param array $user 会员
     */
    public static function updatePwd($params, $admin, $user)
    {
        list($lineId, $type, $data, $password) = array_values($params);

        $db = Util::db();

        // 处理数据
        $nodeColumn = self::updatePwdParams($type, $data, $password);

        // 订单节点数据
        $orderNodeList = self::updatePwdGetNodeList($lineId, $data, $user);

        // 计算时间阈值
        $hourAgo = OptionService::getConfigReserve();

        // 处理订单节点数据
        list($updateOrderNodeData, $apiDataDel, $apiDataAdd) = self::updatePwdProcessNodeList($admin, $user, $nodeColumn, $orderNodeList, $hourAgo);

        // 执行操作
        self::updatePwdExecute($db, $updateOrderNodeData);

        $apiResDel = Util::batchUserApi('del', $apiDataDel);
        $apiResAdd = Util::batchUserApi('add', $apiDataAdd);
        $apiRes = array_merge($apiResDel, $apiResAdd);
        return V(0, $apiRes ? implode(',', $apiRes) : '修改密码成功', []);
    }

    /**
     * 修改密码-处理数据
     **/
    public static function updatePwdParams($type, $data, $password)
    {
        if ($type == 1) {
            foreach ($data as $value) {
                if (!isset($value['id']) || empty($value['id'])) {
                    throw new BusinessException('节点参数错误！');
                }
            }
            $data = array_map(function ($item) use ($password) {
                $item['password'] = $password;
                return $item;
            }, $data);
        } else {
            foreach ($data as $value) {
                if (!isset($value['id']) || empty($value['id'])) {
                    throw new BusinessException('节点参数错误！');
                }
                if (!isset($value['password']) || empty($value['password'])) {
                    throw new BusinessException('请输入密码！');
                }
            }
        }
        $nodeColumn = array_column($data, null, 'id');
        return $nodeColumn;
    }

    /**
     * 修改密码-订单节点数据
     **/
    public static function updatePwdGetNodeList($lineId, $data, $user)
    {
        $ids = array_column($data, 'id');

        $orderNodeList = V1OrderNode::where('deleted', CommonConstant::DELETE_0)
            ->where('user_id', $user['id'])
            ->where('line_id', $lineId)
            ->whereIn('id', $ids)
            ->select(self::node_field())
            ->with(['server'])
            ->get();
        if ($orderNodeList->isEmpty()) {
            throw new BusinessException('没有订单节点！');
        }

        if (count($orderNodeList) != count($ids)) {
            throw new BusinessException('部分订单节点不存在！');
        }

        // 提前检查节点状态
        $expiredNodes = $orderNodeList->where('status', CommonConstant::DEFAULT_2);
        if ($expiredNodes->isNotEmpty()) {
            throw new BusinessException('存在过期节点，不能修改密码');
        }

        $refundNodes = $orderNodeList->where('refund_status', CommonConstant::DEFAULT_1);
        if ($refundNodes->isNotEmpty()) {
            throw new BusinessException('存在退款审核中节点，不能修改密码！');
        }

        $refundNodes = $orderNodeList->where('refund_status', CommonConstant::DEFAULT_2);
        if ($refundNodes->isNotEmpty()) {
            throw new BusinessException('存在退款审核通过节点，不能修改密码！');
        }
        return $orderNodeList;
    }

    /**
     * 修改密码-处理订单节点数据
     */
    public static function updatePwdProcessNodeList($admin, $user, $nodeColumn, $orderNodeList, $hourAgo)
    {
        $updateOrderNodeData = [];
        $apiDataDel = [];
        $apiDataAdd = [];
        foreach ($orderNodeList as $orderNode) {
            $expire = strtotime($orderNode->expire_time);
            if ($expire <= $hourAgo) {
                throw new BusinessException('存在过期节点，不能修改密码！');
            }

            $expireDateTime = $orderNode->expire_time;
            $nodeId = $orderNode->id;
            $nodeInfo = $nodeColumn[$nodeId] ?? null;
            if ($nodeInfo) {
                $orderNodeData = [];
                $orderNodeData['pass'] = $nodeInfo['password'];
                $updateOrderNodeData[$nodeId] = $orderNodeData;
                if ($orderNode->status == CommonConstant::DEFAULT_0) {
                    // 未过期订单
                    // 请求接口数据
                    if ($orderNode->server) {
                        $serverInfo = $orderNode->server;
                        $url = 'http://' . $serverInfo->ip . ':' . $serverInfo->port . '/user/';
                        $apiData = [];
                        $apiData['url'] = $url;
                        $apiData['ip'] = $serverInfo->ip;
                        $apiData['port'] = $serverInfo->port;
                        $apiData['key'] = $serverInfo->key;
                        $apiData['username'] = $orderNode->account;
                        $apiData['password'] = $orderNode->pass;
                        $apiDataDel[] = $apiData;
                        $apiData = [];
                        $apiData['url'] = $url;
                        $apiData['ip'] = $serverInfo->ip;
                        $apiData['port'] = $serverInfo->port;
                        $apiData['key'] = $serverInfo->key;
                        $apiData['username'] = $orderNode->account;
                        $apiData['password'] = $orderNode->pass;
                        $apiData['user_type'] = $serverInfo->line_type; // 线路类型，1.socks5 2.HTTP -1.通用
                        $apiData['limit_up'] = $serverInfo->bandwidth_up;
                        $apiData['limit_down'] = $serverInfo->bandwidth_down;
                        $apiData['maxcon'] = $serverInfo->link_url_number;
                        $apiData['enable'] = true; // 是否启用此用户，用于临时禁用
                        $apiData['shadowtime'] = $expireDateTime; // 到期时间 注意要补0
                        $apiData['info'] = 1; // 预留参数
                        $apiData['bindip'] = $orderNode->ip_ems; // 绑定的内网IP参数，如果为空则不限制
                        $apiData['uid'] = $user['username'];
                        $apiDataAdd[] = $apiData;
                    }
                }
            }
        }
        return [$updateOrderNodeData, $apiDataDel, $apiDataAdd];
    }

    /**
     * 修改密码-执行操作
     **/
    public static function updatePwdExecute($db, $updateOrderNodeData)
    {
        // 批量更新订单节点
        if ($updateOrderNodeData) {
            $db->transaction(function () use ($db, $updateOrderNodeData) {
                $fields = ['pass'];
                list($sql, $allBindings) = Util::buildBatchUpdate('wa_v1_order_node', $fields, $updateOrderNodeData);
                $db->statement($sql, $allBindings);
            });
        }
        return true;
    }


    /**
     * 调换
     *
     * @param array $params
     * @param array $admin 管理员/会员
     * @param array $user 会员
     */
    public static function exchange($params, $admin, $user)
    {
        list($lineId, $type, $accountType, $account, $pass, $ids, $data, $newGameId, $lineIdNew) = array_values($params);

        $db = Util::db();

        $testDuration = [
            CommonConstant::TIME_0,
            CommonConstant::TIME_1,
            CommonConstant::TIME_2
        ];
        $date = date('Y-m-d H:i:s');

        // 订单节点数据
        $orderNodeList = self::exchangeGetNodeList($lineId, $ids, $user);

        // 游戏Ids
        $gameIds = $orderNodeList->pluck('game_id')->unique()->toArray();
        if (count($gameIds) > 1) {
            throw new BusinessException("不能同时调换多个项目！");
        }
        // 游戏ID
        $gameId = $gameIds[0];

        // 类型
        switch ($type) {
            case CommonConstant::DEFAULT_1:
                // 同地区调换
                $msg = '同地区调换';
                list($allocatedNodes, $servers) = self::exchangeSameRegion($db, $lineId, $gameId, $orderNodeList, $testDuration);
                break;
            case CommonConstant::DEFAULT_2:
                // 随机地区调换
                $msg = '随机地区调换';
                list($allocatedNodes, $servers) = self::exchangeRandomRegion($db, $lineId, $gameId, $orderNodeList, $testDuration);
                break;
            case CommonConstant::DEFAULT_3:
                // 自选地区调换
                $msg = '自选地区调换';
                list($allocatedNodes, $servers) = self::exchangeSpecifiedRegion($db, $lineIdNew, $data, $gameId, $orderNodeList, $testDuration);
                break;
            default:
                // 游戏调换
                $msg = '游戏调换';
                list($allocatedNodes, $servers) = self::exchangeGame($db, $lineId, $newGameId, $orderNodeList, $testDuration);
                $gameId = $newGameId;
                break;
        }

        // 处理订单节点数据
        list($fields, $orderData, $updateOrderNodeData, $insertExchangeLogData, $apiDataDel, $apiDataAdd) = self::exchangeProcessNodeList($lineId, $type, $accountType, $account, $pass, $gameId, $lineIdNew, $date, $admin, $user, $allocatedNodes, $servers);

        // 执行操作
        $result = self::exchangeExecute($db, $fields, $orderData, $updateOrderNodeData, $insertExchangeLogData);

        $apiResDel = Util::batchUserApi('del', $apiDataDel);
        $apiResAdd = Util::batchUserApi('add', $apiDataAdd);
        $apiRes = array_merge($apiResDel, $apiResAdd);
        return V(0, $apiRes ? implode(',', $apiRes) : $msg . '成功', $result);
    }

    /**
     * 同地区调换
     */
    public static function exchangeSameRegion($db, $lineId, $gameId, $orderNodeList, $testDuration)
    {
        // 每个地区分组原节点
        $regionNodesOld = $orderNodeList->groupBy('region_id');

        // 地区Ids
        $regionIds = $orderNodeList->pluck('region_id')->unique()->toArray();

        // 节点数据
        $nodes = $db->table('wa_v1_node_static')
            ->where('deleted', CommonConstant::DELETE_0)
            ->where('line_id', $lineId)
            ->whereIn('region_id', $regionIds)
            ->get();
        if ($nodes->isEmpty()) {
            throw new BusinessException('没有可用的节点！');
        }

        // 服务器Ids
        $serverIds = $nodes->pluck('server_id')->unique()->toArray();

        // 服务器数据
        $servers = $db->table('wa_v1_server')
            ->where('deleted', CommonConstant::DELETE_0)
            ->whereIn('id', $serverIds)
            ->get()
            ->keyBy('id');

        // 每个服务器的已售数量（所有游戏和地区的订单）
        $serverUsedCounts = OrderService::getServerUsedCounts($db, $lineId, $serverIds, $testDuration);

        // 当前游戏+地区的已售节点
        $nodeUsed = OrderService::getNodeUsed($db, $lineId, $gameId, $regionIds, $testDuration);

        // 过滤掉已被当前游戏使用的节点
        $availableNodes = $nodes->filter(function ($node) use ($nodeUsed) {
            return !in_array($node->code, $nodeUsed);
        });

        // 重新按地区分组可用节点
        $availableRegionNodes = $availableNodes->groupBy('region_id');

        // 计算每个服务器的空闲数量
        $serverAvailableCounts = [];
        foreach ($servers as $serverId => $server) {
            $usedCount = $serverUsedCounts[$serverId] ?? 0;
            $serverAvailableCounts[$serverId] = max(0, $server->max_limit - $usedCount);
        }

        $allocatedNodes = [];

        // 处理每个地区的节点调换
        foreach ($regionNodesOld as $regionId => $regionNodeListOld) {
            $number = count($regionNodeListOld);

            if (!isset($availableRegionNodes[$regionId])) {
                throw new BusinessException("地区 {$regionId} 在当前游戏下没有可用节点");
            }

            $regionNodeList = $availableRegionNodes[$regionId];

            // 检查当前游戏+地区的可购买数量
            if (count($regionNodeList) < $number) {
                throw new BusinessException("地区 {$regionId} 在当前游戏下可用节点不足，需要 {$number} 个，但只有 " . count($regionNodeList) . " 个可用");
            }

            // 按服务器空闲数量排序节点，然后随机打乱相同空闲数量的节点
            $sortedNodes = OrderService::sortNodesByServerAvailabilityRandom($regionNodeList, $serverAvailableCounts);

            // 分配新节点
            $allocatedCount = 0;
            foreach ($sortedNodes as $node) {
                if ($allocatedCount >= $number) {
                    break;
                }

                // 检查服务器是否还有空闲
                if (($serverAvailableCounts[$node->server_id] ?? 0) <= 0) {
                    continue; // 服务器已满，跳过
                }

//                $allocatedNodes[] = $node;
                $originalNode = $regionNodeListOld->slice($allocatedCount, 1)->first();
                $allocatedNodes[] = [
                    'new_node' => $node,
                    'original_node' => $originalNode
                ];
                $allocatedCount++;

                // 更新服务器空闲数量
                $serverAvailableCounts[$node->server_id]--;
            }

            if ($allocatedCount < $number) {
                throw new BusinessException("地区 {$regionId} 由于服务器限制，只能分配 {$allocatedCount} 个节点，需要 {$number} 个");
            }
        }

        return [$allocatedNodes, $servers];
    }

    /**
     * 随机地区调换
     */
    public static function exchangeRandomRegion($db, $lineId, $gameId, $orderNodeList, $testDuration)
    {
        $totalNodes = count($orderNodeList);

        // 线路下所有可用地区
        $regions = $db->table('wa_v1_region')
            ->where('deleted', CommonConstant::DELETE_0)
            ->where('line_id', $lineId)
            ->select(['id', 'name'])
            ->get();
        if ($regions->isEmpty()) {
            throw new BusinessException('没有可用的地区！');
        }

        // 地区Ids
        $regionIds = $regions->pluck('id')->toArray();

        // 节点列表
        $nodes = $db->table('wa_v1_node_static')
            ->where('deleted', CommonConstant::DELETE_0)
            ->where('line_id', $lineId)
            ->whereIn('region_id', $regionIds)
            ->get();
        if ($nodes->isEmpty()) {
            throw new BusinessException('没有可用的节点！');
        }

        // 服务器Ids
        $serverIds = $nodes->pluck('server_id')->unique()->toArray();

        // 服务器列表
        $servers = $db->table('wa_v1_server')
            ->where('deleted', CommonConstant::DELETE_0)
            ->whereIn('id', $serverIds)
            ->get()
            ->keyBy('id');

        // 每个服务器的已售数量（所有游戏和地区的订单）
        $serverUsedCounts = OrderService::getServerUsedCounts($db, $lineId, $serverIds, $testDuration);

        // 当前游戏所有已售节点
        $nodeUsed = $db->table('wa_v1_order_node')
            ->where('deleted', CommonConstant::DELETE_0)
            ->whereIn('status', [CommonConstant::DEFAULT_0, CommonConstant::DEFAULT_1])
            ->whereIn('refund_status', [CommonConstant::DEFAULT_0, CommonConstant::DEFAULT_1, CommonConstant::DEFAULT_3])
            ->where('line_id', $lineId)
            ->where('game_id', $gameId)
            ->whereNotIn('duration', $testDuration)
            ->pluck('code')
            ->toArray();

        // 过滤掉已被当前游戏使用的节点
        $availableNodes = $nodes->filter(function ($node) use ($nodeUsed) {
            return !in_array($node->code, $nodeUsed);
        });

        // 计算每个服务器的空闲数量
        $serverAvailableCounts = [];
        foreach ($servers as $serverId => $server) {
            $usedCount = $serverUsedCounts[$serverId] ?? 0;
            $serverAvailableCounts[$serverId] = max(0, $server->max_limit - $usedCount);
        }

        // 按地区分组可用节点
        $availableRegionNodes = $availableNodes->groupBy('region_id');

        // 找出有足够可用节点的地区
        $eligibleRegions = [];
        foreach ($availableRegionNodes as $regionId => $regionNodes) {
            // 计算该地区实际可用的节点数（考虑服务器限制）
            $regionAvailableCount = 0;
            foreach ($regionNodes as $node) {
                if (($serverAvailableCounts[$node->server_id] ?? 0) > 0) {
                    $regionAvailableCount++;
                }
            }

            if ($regionAvailableCount >= $totalNodes) {
                $eligibleRegions[] = $regionId;
            }
        }

        if (empty($eligibleRegions)) {
            throw new BusinessException('没有足够的可用节点进行随机地区调换！');
        }

        // 随机选择一个地区
        $randomRegionId = $eligibleRegions[array_rand($eligibleRegions)];
        $selectedRegionNodes = $availableRegionNodes[$randomRegionId];

        // 按服务器空闲数量排序节点，然后随机打乱相同空闲数量的节点
        $sortedNodes = OrderService::sortNodesByServerAvailabilityRandom($selectedRegionNodes, $serverAvailableCounts);

        $allocatedNodes = [];
        $allocatedCount = 0;

        // 分配新节点
        foreach ($sortedNodes as $node) {
            if ($allocatedCount >= $totalNodes) {
                break;
            }

            // 检查服务器是否还有空闲
            if (($serverAvailableCounts[$node->server_id] ?? 0) <= 0) {
                continue;
            }

            $originalNode = $orderNodeList->slice($allocatedCount, 1)->first();
            $allocatedNodes[] = [
                'new_node' => $node,
                'original_node' => $originalNode
            ];
            $allocatedCount++;

            // 更新服务器空闲数量
            $serverAvailableCounts[$node->server_id]--;
        }

        if ($allocatedCount < $totalNodes) {
            throw new BusinessException("随机地区调换由于服务器限制，只能分配 {$allocatedCount} 个节点，需要 {$totalNodes} 个");
        }

        return [$allocatedNodes, $servers];
        return [
            'random_region_id' => $randomRegionId,
            'region_name' => $regions->where('id', $randomRegionId)->first()->name ?? '未知地区'
        ];
    }

    /**
     * 自选地区调换
     */
    private static function exchangeSpecifiedRegion($db, $lineIdNew, $data, $gameId, $orderNodeList, $testDuration)
    {
        // 节点数据
        $totalRequested = 0;
        $regionIds = [];
        $regionRequests = [];
        foreach ($data as $value) {
            $totalRequested += (int)$value['number'];
            $regionIds[] = $value['region_id'];
            $regionRequests[$value['region_id']] = (int)$value['number'];
        }

        // 验证节点数量是否匹配
        $totalOrderNodes = count($orderNodeList);
        if ($totalRequested != $totalOrderNodes) {
            throw new BusinessException("自选地区的节点总数({$totalRequested})与原节点数量({$totalOrderNodes})不匹配");
        }

        // 地区列表
        $regions = $db->table('wa_v1_region')
            ->where('deleted', CommonConstant::DELETE_0)
            ->where('line_id', $lineIdNew)
            ->whereIn('id', $regionIds)
            ->select(['id', 'name'])
            ->get();
        if ($regions->isEmpty()) {
            throw new BusinessException('地区不存在！');
        }
        if (count($regions) != count($regionIds)) {
            throw new BusinessException('部分地区不存在！');
        }

        // 地区Ids
        $regionIds = $regions->pluck('id')->toArray();

        // 节点列表
        $nodes = $db->table('wa_v1_node_static')
            ->where('deleted', CommonConstant::DELETE_0)
            ->where('line_id', $lineIdNew)
            ->whereIn('region_id', $regionIds)
            ->get();
        if ($nodes->isEmpty()) {
            throw new BusinessException('没有可用的节点！');
        }

        // 服务器Ids
        $serverIds = $nodes->pluck('server_id')->unique()->toArray();

        // 服务器列表
        $servers = $db->table('wa_v1_server')
            ->where('deleted', CommonConstant::DELETE_0)
            ->whereIn('id', $serverIds)
            ->get()
            ->keyBy('id');

        // 每个服务器的已售数量（所有游戏和地区的订单）
        $serverUsedCounts = OrderService::getServerUsedCounts($db, $lineIdNew, $serverIds, $testDuration);

        // 当前游戏+地区的已售节点
        $nodeUsed = OrderService::getNodeUsed($db, $lineIdNew, $gameId, $regionIds, $testDuration);

        // 过滤掉已被当前游戏使用的节点
        $availableNodes = $nodes->filter(function ($node) use ($nodeUsed) {
            return !in_array($node->code, $nodeUsed);
        });

        // 重新按地区分组可用节点
        $availableRegionNodes = $availableNodes->groupBy('region_id');

        // 计算每个服务器的空闲数量
        $serverAvailableCounts = [];
        foreach ($servers as $serverId => $server) {
            $usedCount = $serverUsedCounts[$serverId] ?? 0;
            $serverAvailableCounts[$serverId] = max(0, $server->max_limit - $usedCount);
        }

        $allocatedNodes = [];

        // 处理每个地区的节点分配
        foreach ($regionRequests as $regionId => $number) {
            if (!isset($availableRegionNodes[$regionId])) {
                throw new BusinessException("地区 {$regionId} 在当前游戏下没有可用节点");
            }

            $regionNodeList = $availableRegionNodes[$regionId];

            // 检查当前游戏+地区的可购买数量
            if (count($regionNodeList) < $number) {
                throw new BusinessException("地区 {$regionId} 在当前游戏下可用节点不足，需要 {$number} 个，但只有 " . count($regionNodeList) . " 个可用");
            }

            // 按服务器空闲数量排序节点，然后随机打乱相同空闲数量的节点
            $sortedNodes = OrderService::sortNodesByServerAvailabilityRandom($regionNodeList, $serverAvailableCounts);

            // 分配新节点
            $allocatedCount = 0;
            foreach ($sortedNodes as $node) {
                if ($allocatedCount >= $number) {
                    break;
                }

                // 检查服务器是否还有空闲
                if (($serverAvailableCounts[$node->server_id] ?? 0) <= 0) {
                    continue; // 服务器已满，跳过
                }

                // 从原节点列表中按顺序取出一个节点
                $originalNode = $orderNodeList->shift();
                $allocatedNodes[] = [
                    'new_node' => $node,
                    'original_node' => $originalNode
                ];
                $allocatedCount++;

                // 更新服务器空闲数量
                $serverAvailableCounts[$node->server_id]--;
            }

            if ($allocatedCount < $number) {
                throw new BusinessException("地区 {$regionId} 由于服务器限制，只能分配 {$allocatedCount} 个节点，需要 {$number} 个");
            }
        }

        return [$allocatedNodes, $servers];
    }

    /**
     * 游戏可售列表
     *
     * @param array $params
     * @param array $admin 管理员/会员
     * @param array $user 会员
     **/
    public static function getGameListExchange($params, $admin, $user)
    {
        list($lineId, $ids) = array_values($params);

        $db = Util::db();

        $testDuration = [
            CommonConstant::TIME_0,
            CommonConstant::TIME_1,
            CommonConstant::TIME_2
        ];

        // 获取订单节点数据
        $orderNodeList = self::exchangeGetNodeList($lineId, $ids, $user);

        // 计算需要调换的节点数量
        $requiredNodes = count($orderNodeList);

        // 获取原节点的地区分布
        $regionNodesOld = $orderNodeList->groupBy('region_id');
        $regionIds = $orderNodeList->pluck('region_id')->unique()->toArray();

        // 获取线路下所有游戏
        $games = $db->table('wa_v1_game')
            ->where('deleted', CommonConstant::DELETE_0)
            ->select('id', 'name')
            ->get();
        if ($games->isEmpty()) {
            throw new BusinessException('没有可用的游戏！');
        }

        // 节点列表
        $nodes = $db->table('wa_v1_node_static')
            ->where('deleted', CommonConstant::DELETE_0)
            ->where('line_id', $lineId)
            ->whereIn('region_id', $regionIds)
            ->get();
        if ($nodes->isEmpty()) {
            throw new BusinessException('没有可用的节点！');
        }

        // 服务器Ids
        $serverIds = $nodes->pluck('server_id')->unique()->toArray();

        // 服务器列表
        $servers = $db->table('wa_v1_server')
            ->where('deleted', CommonConstant::DELETE_0)
            ->whereIn('id', $serverIds)
            ->get()
            ->keyBy('id');

        // 每个服务器的已售数量（所有游戏和地区的订单）
        $serverUsedCounts = OrderService::getServerUsedCounts($db, $lineId, $serverIds, $testDuration);

        // 计算每个服务器的空闲数量
        $serverAvailableCounts = [];
        foreach ($servers as $serverId => $server) {
            $usedCount = $serverUsedCounts[$serverId] ?? 0;
            $serverAvailableCounts[$serverId] = max(0, $server->max_limit - $usedCount);
        }

        $gameSaleableList = [];

        // 计算每个游戏的可售数量
        foreach ($games as $game) {
            $gameId = $game->id;

            // 查询当前游戏+地区的已售节点
            $nodeUsed = OrderService::getNodeUsed($db, $lineId, $gameId, $regionIds, $testDuration);

            // 过滤掉已被当前游戏使用的节点
            $availableNodes = $nodes->filter(function ($node) use ($nodeUsed) {
                return !in_array($node->code, $nodeUsed);
            });

            // 重新按地区分组可用节点
            $availableRegionNodes = $availableNodes->groupBy('region_id');

            // 计算每个游戏在每个地区的可售数量
            $regionSaleableCounts = [];
            $totalSaleable = 0;

            foreach ($regionNodesOld as $regionId => $regionNodeListOld) {
                $requiredInRegion = count($regionNodeListOld);

                if (!isset($availableRegionNodes[$regionId])) {
                    $regionSaleableCounts[$regionId] = 0;
                    continue;
                }

                $regionNodeList = $availableRegionNodes[$regionId];

                // 按服务器空闲数量排序节点
                $sortedNodes = OrderService::sortNodesByServerAvailability($regionNodeList, $serverAvailableCounts);

                // 计算该地区实际可用的节点数（考虑服务器限制）
                $regionAvailableCount = 0;
                $tempServerCounts = $serverAvailableCounts; // 临时副本，避免影响其他游戏的计算

                foreach ($sortedNodes as $node) {
                    if ($regionAvailableCount >= $requiredInRegion) {
                        break;
                    }

                    // 检查服务器是否还有空闲
                    if (($tempServerCounts[$node->server_id] ?? 0) <= 0) {
                        continue;
                    }

                    $regionAvailableCount++;
                    $tempServerCounts[$node->server_id]--;
                }

                $regionSaleableCounts[$regionId] = $regionAvailableCount;
                $totalSaleable += $regionAvailableCount;
            }

            // 检查该游戏是否满足调换条件
            $canExchange = true;
            foreach ($regionNodesOld as $regionId => $regionNodeListOld) {
                $requiredInRegion = count($regionNodeListOld);
                if ($regionSaleableCounts[$regionId] < $requiredInRegion) {
                    $canExchange = false;
                    break;
                }
            }

            $gameSaleableList[] = [
                'game_id' => $game->id,
                'game_name' => $game->name,
                'saleable_number' => $totalSaleable,
                'can_exchange' => $canExchange,
                'required_nodes' => $requiredNodes,
                'region_details' => $regionSaleableCounts
            ];
        }

        // 按可售数量降序排序
        usort($gameSaleableList, function ($a, $b) {
            if ($a['can_exchange'] != $b['can_exchange']) {
                return $b['can_exchange'] - $a['can_exchange'];
            }
            return $b['saleable_number'] - $a['saleable_number'];
        });

        return $gameSaleableList;
    }

    /**
     * 游戏调换
     */
    private static function exchangeGame($db, $lineId, $newGameId, $orderNodeList, $testDuration)
    {
        // 每个地区分组原节点
        $regionNodesOld = $orderNodeList->groupBy('region_id');

        // 地区Ids
        $regionIds = $orderNodeList->pluck('region_id')->unique()->toArray();

        // 节点列表
        $nodes = $db->table('wa_v1_node_static')
            ->where('deleted', CommonConstant::DELETE_0)
            ->where('line_id', $lineId)
            ->whereIn('region_id', $regionIds)
            ->get();
        if ($nodes->isEmpty()) {
            throw new BusinessException('没有可用的节点！');
        }

        // 服务器Ids
        $serverIds = $nodes->pluck('server_id')->unique()->toArray();

        // 服务器列表
        $servers = $db->table('wa_v1_server')
            ->where('deleted', CommonConstant::DELETE_0)
            ->whereIn('id', $serverIds)
            ->get()
            ->keyBy('id');

        // 每个服务器的已售数量（所有游戏和地区的订单）
        $serverUsedCounts = OrderService::getServerUsedCounts($db, $lineId, $serverIds, $testDuration);

        // 新游戏+地区的已售节点
        $nodeUsed = OrderService::getNodeUsed($db, $lineId, $newGameId, $regionIds, $testDuration);

        // 过滤掉已被新游戏使用的节点
        $availableNodes = $nodes->filter(function ($node) use ($nodeUsed) {
            return !in_array($node->code, $nodeUsed);
        });

        // 重新按地区分组可用节点
        $availableRegionNodes = $availableNodes->groupBy('region_id');

        // 计算每个服务器的空闲数量
        $serverAvailableCounts = [];
        foreach ($servers as $serverId => $server) {
            $usedCount = $serverUsedCounts[$serverId] ?? 0;
            $serverAvailableCounts[$serverId] = max(0, $server->max_limit - $usedCount);
        }

        $allocatedNodes = [];

        // 处理每个地区的节点调换
        foreach ($regionNodesOld as $regionId => $regionNodeListOld) {
            $number = count($regionNodeListOld);

            if (!isset($availableRegionNodes[$regionId])) {
                throw new BusinessException("地区 {$regionId} 在新游戏下没有可用节点");
            }

            $regionNodeList = $availableRegionNodes[$regionId];

            // 检查新游戏+地区的可购买数量
            if (count($regionNodeList) < $number) {
                throw new BusinessException("地区 {$regionId} 在新游戏下可用节点不足，需要 {$number} 个，但只有 " . count($regionNodeList) . " 个可用");
            }

            // 按服务器空闲数量排序节点，然后随机打乱相同空闲数量的节点
            $sortedNodes = OrderService::sortNodesByServerAvailabilityRandom($regionNodeList, $serverAvailableCounts);

            // 分配新节点
            $allocatedCount = 0;
            foreach ($sortedNodes as $node) {
                if ($allocatedCount >= $number) {
                    break;
                }

                // 检查服务器是否还有空闲
                if (($serverAvailableCounts[$node->server_id] ?? 0) <= 0) {
                    continue; // 服务器已满，跳过
                }

                $originalNode = $regionNodeListOld->slice($allocatedCount, 1)->first();
                $allocatedNodes[] = [
                    'new_node' => $node,
                    'original_node' => $originalNode
                ];
                $allocatedCount++;

                // 更新服务器空闲数量
                $serverAvailableCounts[$node->server_id]--;
            }

            if ($allocatedCount < $number) {
                throw new BusinessException("地区 {$regionId} 由于服务器限制，只能分配 {$allocatedCount} 个节点，需要 {$number} 个");
            }
        }

        return [$allocatedNodes, $servers];
    }

    /**
     * 调换-订单节点数据
     **/
    public static function exchangeGetNodeList($lineId, $ids, $user)
    {
        $orderNodeList = V1OrderNode::where('deleted', CommonConstant::DELETE_0)
            ->where('user_id', $user['id'])
            ->where('line_id', $lineId)
            ->whereIn('id', $ids)
            ->select(self::node_field())
            ->get();
        if ($orderNodeList->isEmpty()) {
            throw new BusinessException('没有订单节点！');
        }

        if (count($orderNodeList) != count($ids)) {
            throw new BusinessException('部分订单节点不存在！');
        }

        // 提前检查节点状态
        $expiredNodes = $orderNodeList->where('status', CommonConstant::DEFAULT_1);
        if ($expiredNodes->isNotEmpty()) {
            throw new BusinessException('存在到期节点，不能调换！');
        }

        // 提前检查节点状态
        $expiredNodes = $orderNodeList->where('status', CommonConstant::DEFAULT_2);
        if ($expiredNodes->isNotEmpty()) {
            throw new BusinessException('存在过期节点，不能调换！');
        }

        $refundNodes = $orderNodeList->where('refund_status', CommonConstant::DEFAULT_1);
        if ($refundNodes->isNotEmpty()) {
            throw new BusinessException('存在退款审核中节点，不能调换！');
        }

        $refundNodes = $orderNodeList->where('refund_status', CommonConstant::DEFAULT_2);
        if ($refundNodes->isNotEmpty()) {
            throw new BusinessException('存在退款审核通过节点，不能调换！');
        }
        return $orderNodeList;
    }

    /**
     * 调换-处理订单节点数据
     **/
    public static function exchangeProcessNodeList($lineId, $type, $accountType, $account, $pass, $gameId, $lineIdNew, $date, $admin, $user, $allocatedNodes, $servers)
    {
        $fields = ['server_id', 'code', 'ip', 'port', 'ip_ems', 'account', 'pass', 'domain', 'data'];
        if ($type == CommonConstant::DEFAULT_3 && $lineId != $lineIdNew) {
            $fields[] = 'order_id';
            $fields[] = 'order_no';
            $fields[] = 'line_id';
            $fields[] = 'region_id';
        }
        if ($type == CommonConstant::DEFAULT_4) {
            $fields[] = 'game_id';
        }

        $orderData = [];
        $updateOrderNodeData = [];
        $insertExchangeLogData = [];
        $apiDataDel = [];
        $apiDataAdd = [];
        foreach ($allocatedNodes as $key => $item) {
            $node = $item['new_node'];
            $originalNode = $item['original_node'];

            $expireDateTime = $originalNode->expire_time;
            switch ($accountType) {
                case CommonConstant::DEFAULT_0:
                    $nodeAccount = $originalNode->account;
                    $nodePass = $originalNode->pass;
                    break;
                default:
                    $nodeAccount = $account;
                    $nodePass = $pass;
            }

            $orderNodeData = [];
            if ($type == CommonConstant::DEFAULT_3 && $lineId != $lineIdNew) {
                if (!$orderData) {
                    $orderNo = get_order_sn($user['id']);
                    $orderData['user_id'] = $user['id'];
                    $orderData['user_name'] = $user['username'];
                    $orderData['order_no'] = $orderNo;
                    $orderData['line_id'] = $lineIdNew;
                    $orderData['game_id'] = $gameId;
                    $orderData['duration'] = $originalNode->duration;
                    $orderData['expire_time'] = $originalNode->expire_time;
                    $orderData['remark'] = $originalNode->remark;
                    $orderData['source'] = CommonConstant::DEFAULT_0;
                }

                // 跟踪订单的最大过期时间
                if ($originalNode->expire_time > $orderData['expire_time']) {
                    $orderData['duration'] = $originalNode->duration;
                    $orderData['expire_time'] = $originalNode->expire_time;
                }

                $orderNodeData['region_id'] = $node->region_id;
            }
            if ($type == CommonConstant::DEFAULT_4) {
                $orderNodeData['game_id'] = $gameId;
            }
            $orderNodeData['server_id'] = $node->server_id;
            $orderNodeData['code'] = $node->code;
            $orderNodeData['ip'] = $node->ip;
            $orderNodeData['port'] = $node->port;
            $orderNodeData['ip_ems'] = $node->ip_ems;
            $orderNodeData['account'] = $nodeAccount;
            $orderNodeData['pass'] = $nodePass;
            $server = $servers->get($node->server_id);
            $url = 'http://' . $server->ip . ':' . $server->port . '/user/';
            $orderNodeData['domain'] = $server->domain;
            $orderNodeData['data'] = $url . '|' . $server->key;
            $updateOrderNodeData[$originalNode->id] = $orderNodeData;

            $exchangeLogData = [];
            $exchangeLogData['created_at'] = $date;
            $exchangeLogData['user_id'] = $user['id'];
            $exchangeLogData['user_name'] = $user['username'];
            $exchangeLogData['line_id'] = $lineId;
            $exchangeLogData['type'] = CommonConstant::DEFAULT_1;
            $exchangeLogData['original_info'] = $originalNode->ip . '|' . $originalNode->port . '|' . $originalNode->account . '|' . $originalNode->pass . '|' . $expireDateTime;
            $exchangeLogData['new_info'] = $node->ip . '|' . $node->port . '|' . $nodeAccount . '|' . $nodePass . '|' . $expireDateTime;
            $exchangeLogData['order_id'] = $originalNode->order_id;
            $exchangeLogData['order_node_id'] = $originalNode->id;
            $insertExchangeLogData[] = $exchangeLogData;

            $array = explode('|', $originalNode->data);
            $apiData = [];
            $apiData['url'] = $array[0];
//            $apiData['ip'] = $originalNode->ip;
//            $apiData['port'] = $originalNode->port;
            $apiData['key'] = $array[1];
            $apiData['username'] = $originalNode->account;
            $apiData['password'] = $originalNode->pass;
            $apiDataDel[] = $apiData;
            $apiData = [];
            $apiData['url'] = $url;
            $apiData['ip'] = $server->ip;
            $apiData['port'] = $server->port;
            $apiData['key'] = $server->key;
            $apiData['username'] = $nodeAccount;
            $apiData['password'] = $nodePass;
            $apiData['user_type'] = $server->line_type; // 线路类型，1.socks5 2.HTTP -1.通用
            $apiData['limit_up'] = $server->bandwidth_up;
            $apiData['limit_down'] = $server->bandwidth_down;
            $apiData['maxcon'] = $server->link_url_number;
            $apiData['enable'] = true; // 是否启用此用户，用于临时禁用
            $apiData['shadowtime'] = $expireDateTime; // 到期时间 注意要补0
            $apiData['info'] = 1; // 预留参数
            $apiData['bindip'] = $node->ip_ems; // 绑定的内网IP参数，如果为空则不限制
            $apiData['uid'] = $user['username'];
            $apiDataAdd[] = $apiData;
        }

        return [$fields, $orderData, $updateOrderNodeData, $insertExchangeLogData, $apiDataDel, $apiDataAdd];
    }

    /**
     * 调换-执行操作
     **/
    public static function exchangeExecute($db, $fields, $orderData, $updateOrderNodeData, $insertExchangeLogData)
    {
        $result = $db->transaction(function () use ($db, $fields, $orderData, $updateOrderNodeData, $insertExchangeLogData) {
            if ($orderData) {
                // 添加订单
                $orderId = doInsert($orderData, new V1Order());

                $updateOrderNodeData = array_map(function ($item) use ($orderId, $orderData) {
                    $item['order_id'] = $orderId;
                    $item['order_no'] = $orderData['order_no'];
                    $item['line_id'] = $orderData['line_id'];
                    return $item;
                }, $updateOrderNodeData);

                $insertExchangeLogData = array_map(function ($item) use ($orderId, $orderData) {
                    $item['order_id'] = $orderId;
                    $item['line_id'] = $orderData['line_id'];
                    return $item;
                }, $insertExchangeLogData);
            }

            // 批量更新订单节点
            list($sql, $allBindings) = Util::buildBatchUpdate('wa_v1_order_node', $fields, $updateOrderNodeData);
            $db->statement($sql, $allBindings);

            // 增加订单节点调换数量
            $db->table('wa_v1_order_node')->whereIn('id', array_keys($updateOrderNodeData))->update(['exchange_number' => $db->raw('exchange_number + 1')]);

            // 添加订单节点变动
            $db->table('wa_v1_order_node_exchange_log')->insert($insertExchangeLogData);

            return $updateOrderNodeData;
        });
        return array_values($result);
    }


    /**
     * 自动续费开启/关闭
     *
     * @param array $params
     * @param array $admin 管理员/会员
     * @param array $user 会员
     */
    public static function renewAutoOpen($params, $admin, $user)
    {
        list($ids, $status) = array_values($params);

        $db = Util::db();

        // 处理数据
        self::renewAutoOpenParams($status);

        // 订单节点数据
        $orderNodeList = self::renewAutoOpenGetNodeList($ids, $status, $user);

        // 计算时间阈值
        $hourAgo = OptionService::getConfigReserve();

        // 处理订单节点数据
        self::renewAutoOpenProcessNodeList($status, $orderNodeList, $hourAgo);

        // 执行操作
        self::renewAutoOpenExecute($db, $status, $orderNodeList);

        return true;
    }

    /**
     * 自动续费开启/关闭-处理数据
     **/
    public static function renewAutoOpenParams($status)
    {
        if ($status == CommonConstant::DEFAULT_1) {
            $config = OptionService::getConfig('order');
            if (empty($config['order_node_renew_auto'])) {
                throw new BusinessException('自动续费功能未开启！');
            }
        }
        return true;
    }

    /**
     * 自动续费开启/关闭-订单节点数据
     **/
    public static function renewAutoOpenGetNodeList($ids, $status, $user)
    {
        $orderNodeList = V1OrderNode::where('deleted', CommonConstant::DELETE_0)
            ->where('user_id', $user['id'])
            ->whereIn('id', $ids)
            ->select(self::node_field())
            ->get();
        if ($orderNodeList->isEmpty()) {
            throw new BusinessException('没有订单节点！');
        }

        if (count($orderNodeList) != count($ids)) {
            throw new BusinessException('部分订单节点不存在！');
        }

        if ($status == CommonConstant::DEFAULT_1) {

            // 提前检查节点状态
            $expiredNodes = $orderNodeList->where('status', CommonConstant::DEFAULT_1);
            if ($expiredNodes->isNotEmpty()) {
                throw new BusinessException('存在到期节点，不能开启自动续费！');
            }

            // 提前检查节点状态
            $expiredNodes = $orderNodeList->where('status', CommonConstant::DEFAULT_2);
            if ($expiredNodes->isNotEmpty()) {
                throw new BusinessException('存在过期节点，不能开启自动续费');
            }

            $refundNodes = $orderNodeList->where('refund_status', CommonConstant::DEFAULT_1);
            if ($refundNodes->isNotEmpty()) {
                throw new BusinessException('存在退款审核中节点，不能开启自动续费！');
            }

            $refundNodes = $orderNodeList->where('refund_status', CommonConstant::DEFAULT_2);
            if ($refundNodes->isNotEmpty()) {
                throw new BusinessException('存在退款审核通过节点，不能开启自动续费！');
            }
        }
        return $orderNodeList;
    }

    /**
     * 自动续费开启/关闭-处理订单节点数据
     */
    public static function renewAutoOpenProcessNodeList($status, $orderNodeList, $hourAgo)
    {
        if ($status == CommonConstant::DEFAULT_1) {
            foreach ($orderNodeList as $orderNode) {
                $expire = strtotime($orderNode->expire_time);
                if ($expire <= $hourAgo) {
                    throw new BusinessException('存在过期节点，不能开启自动续费！');
                }
            }
        }
        return true;
    }

    /**
     * 自动续费开启/关闭-执行操作
     **/
    public static function renewAutoOpenExecute($db, $status, $orderNodeList)
    {
        $db->transaction(function () use ($db, $status, $orderNodeList) {
            $orderNodeIds = $orderNodeList->pluck('id')->toArray();
            $orderNodeData['renew_status'] = $status;
            $db->table('wa_v1_order_node')->whereIn('id', $orderNodeIds)->update($orderNodeData);
        });
        return true;
    }


    /**
     * 解绑
     *
     * @param array $params
     * @param array $admin 管理员/会员
     * @param array $user 会员
     */
    public static function bind($params, $admin, $user)
    {
        list($ids) = array_values($params);

        // 订单节点数据
        $orderNodeList = self::bindGetNodeList($ids, $user);

        // 计算时间阈值
        $hourAgo = OptionService::getConfigReserve();

        // 处理订单节点数据
        list($apiDataDel, $apiDataAdd) = self::bindProcessNodeList($admin, $user, $orderNodeList, $hourAgo);

        $apiResDel = Util::batchUserApi('del', $apiDataDel);
        $apiResAdd = Util::batchUserApi('add', $apiDataAdd);
        $apiRes = array_merge($apiResDel, $apiResAdd);
        return V(0, $apiRes ? implode(',', $apiRes) : '解绑成功', []);
    }

    /**
     * 解绑-订单节点数据
     **/
    public static function bindGetNodeList($ids, $user)
    {
        $orderNodeList = V1OrderNode::where('deleted', CommonConstant::DELETE_0)
            ->where('user_id', $user['id'])
            ->whereIn('id', $ids)
            ->select(self::node_field())
            ->get();
        if ($orderNodeList->isEmpty()) {
            throw new BusinessException('没有订单节点！');
        }

        if (count($orderNodeList) != count($ids)) {
            throw new BusinessException('部分订单节点不存在！');
        }

        // 提前检查节点状态
        $expiredNodes = $orderNodeList->where('status', CommonConstant::DEFAULT_1);
        if ($expiredNodes->isNotEmpty()) {
            throw new BusinessException('存在到期节点，不能解绑！');
        }

        // 提前检查节点状态
        $expiredNodes = $orderNodeList->where('status', CommonConstant::DEFAULT_2);
        if ($expiredNodes->isNotEmpty()) {
            throw new BusinessException('存在过期节点，不能解绑');
        }

        $refundNodes = $orderNodeList->where('refund_status', CommonConstant::DEFAULT_1);
        if ($refundNodes->isNotEmpty()) {
            throw new BusinessException('存在退款审核中节点，不能解绑！');
        }

        $refundNodes = $orderNodeList->where('refund_status', CommonConstant::DEFAULT_2);
        if ($refundNodes->isNotEmpty()) {
            throw new BusinessException('存在退款审核通过节点，不能解绑！');
        }

        return $orderNodeList;
    }

    /**
     * 解绑-处理订单节点数据
     */
    public static function bindProcessNodeList($admin, $user, $orderNodeList, $hourAgo)
    {
        $apiDataDel = [];
        $apiDataAdd = [];
        foreach ($orderNodeList as $orderNode) {
            $expire = strtotime($orderNode->expire_time);
            if ($expire <= $hourAgo) {
                throw new BusinessException('存在过期节点，不能解绑！');
            }

            $expireDateTime = $orderNode->expire_time;

            // 请求接口数据
            if ($orderNode->server) {
                $serverInfo = $orderNode->server;
                $url = 'http://' . $serverInfo->ip . ':' . $serverInfo->port . '/user/';
                $apiData = [];
                $apiData['url'] = $url;
                $apiData['ip'] = $serverInfo->ip;
                $apiData['port'] = $serverInfo->port;
                $apiData['key'] = $serverInfo->key;
                $apiData['username'] = $orderNode->account;
                $apiData['password'] = $orderNode->pass;
                $apiDataDel[] = $apiData;
                $apiData = [];
                $apiData['url'] = $url;
                $apiData['ip'] = $serverInfo->ip;
                $apiData['port'] = $serverInfo->port;
                $apiData['key'] = $serverInfo->key;
                $apiData['username'] = $orderNode->account;
                $apiData['password'] = $orderNode->pass;
                $apiData['user_type'] = $serverInfo->line_type; // 线路类型，1.socks5 2.HTTP -1.通用
                $apiData['limit_up'] = $serverInfo->bandwidth_up;
                $apiData['limit_down'] = $serverInfo->bandwidth_down;
                $apiData['maxcon'] = $serverInfo->link_url_number;
                $apiData['enable'] = true; // 是否启用此用户，用于临时禁用
                $apiData['shadowtime'] = $expireDateTime; // 到期时间 注意要补0
                $apiData['info'] = 1; // 预留参数
                $apiData['bindip'] = $orderNode->ip_ems; // 绑定的内网IP参数，如果为空则不限制
                $apiData['uid'] = $user['username'];
                $apiDataAdd[] = $apiData;
            }
        }

        return [
            $apiDataDel,
            $apiDataAdd,
        ];
    }


    /**
     * 申请退款
     **/
    public static function createRefund($params, $admin)
    {
        list($lineId, $ids) = array_values($params);

        $db = Util::db();

        // 订单节点数据
        $orderNodeList = self::createRefundGetNodeList($lineId, $ids, $admin);

        // 计算时间阈值
        $hourAgo = OptionService::getConfigReserve();

        // 处理订单节点数据
        list($insertRefundData) = self::createRefundProcessNodeList($lineId, $admin, $orderNodeList, $hourAgo);

        // 执行操作
        self::createRefundOpenExecute($db, $orderNodeList, $insertRefundData);

        return true;
    }

    /**
     * 申请退款-订单节点数据
     **/
    public static function createRefundGetNodeList($lineId, $ids, $admin)
    {
        $orderNodeList = V1OrderNode::where('deleted', CommonConstant::DELETE_0)
            ->where('user_id', $admin['id'])
            ->where('line_id', $lineId)
            ->whereIn('id', $ids)
            ->select(self::node_field())
            ->get();
        if ($orderNodeList->isEmpty()) {
            throw new BusinessException('没有订单节点！');
        }

        if (count($orderNodeList) != count($ids)) {
            throw new BusinessException('部分订单节点不存在！');
        }

        // 提前检查节点状态
        $sourceNodes = $orderNodeList->where('source', CommonConstant::DEFAULT_1);
        if ($sourceNodes->isNotEmpty()) {
            throw new BusinessException('存在转户节点，不能申请退款');
        }

        // 提前检查节点状态
        $expiredNodes = $orderNodeList->where('status', CommonConstant::DEFAULT_1);
        if ($expiredNodes->isNotEmpty()) {
            throw new BusinessException('存在到期节点，不能申请退款');
        }

        // 提前检查节点状态
        $expiredNodes = $orderNodeList->where('status', CommonConstant::DEFAULT_2);
        if ($expiredNodes->isNotEmpty()) {
            throw new BusinessException('存在过期节点，不能申请退款');
        }

        $refundNodes = $orderNodeList->where('refund_status', CommonConstant::DEFAULT_1);
        if ($refundNodes->isNotEmpty()) {
            throw new BusinessException('存在退款审核中节点，不能申请退款');
        }

        $refundNodes = $orderNodeList->where('refund_status', CommonConstant::DEFAULT_2);
        if ($refundNodes->isNotEmpty()) {
            throw new BusinessException('存在退款审核通过节点，不能申请退款');
        }

        return $orderNodeList;
    }

    /**
     * 申请退款-处理订单节点数据
     */
    public static function createRefundProcessNodeList($lineId, $admin, $orderNodeList, $hourAgo)
    {
        // 剩余节点退款数量
        $refundParams = orderService::surplusRefundNumber($admin, $lineId);
        if ($refundParams['surplus_refund_number'] == 0) {
            throw new BusinessException('今日节点退款数量已用完！');
        }
        $seconds = $refundParams['order_node_refund_time'] * 3600;
        $time = time();

        $insertRefundData = [];
        foreach ($orderNodeList as $orderNode) {
            $expire = strtotime($orderNode->expire_time);
            if ($expire <= $hourAgo) {
                throw new BusinessException('存在过期节点，不能申请退款！');
            }

            $isTestDuration = in_array($orderNode->duration, [
                CommonConstant::TIME_0,
                CommonConstant::TIME_1,
                CommonConstant::TIME_2
            ]);
            if ($isTestDuration) {
                throw new BusinessException('存在测试卡节点，不能申请退款！');
            }

            $is_refund = 0; // 是否可以退款:0=否,1=是
            $later = strtotime($orderNode->created_at) + $seconds;
            if ($time <= $later) {
                $is_refund = 1;
            }
            if ($is_refund == 0) {
                throw new BusinessException('存在超过退款时限节点，不能申请退款！');
            }

            $refundData['user_id'] = $admin['id'];
            $refundData['order_node_id'] = $orderNode->id;
            $refundData['line_id'] = $lineId;
            $refundData['refund_status'] = CommonConstant::DEFAULT_1;
            $insertRefundData[] = $refundData;
        }
        return [$insertRefundData];
    }

    /**
     * 申请退款-执行操作
     **/
    public static function createRefundOpenExecute($db, $orderNodeList, $insertRefundData)
    {
        $db->transaction(function () use ($db, $orderNodeList, $insertRefundData) {
            $orderNodeIds = $orderNodeList->pluck('id')->toArray();
            $data['renew_status'] = CommonConstant::DEFAULT_0;
            $data['refund_status'] = CommonConstant::DEFAULT_1;
            $data['refund_time'] = date('Y-m-d H:i:s');
            $db->table('wa_v1_order_node')->whereIn('id', $orderNodeIds)->update($data);
            $db->table('wa_v1_order_node_refund_log')->insert($insertRefundData);
        });
        return true;
    }


    /**
     * 后台退款审核
     *
     * @param array $params
     * @param array $admin
     * @return array
     */
    public static function auditRefund($params, $admin)
    {
        list($ids, $status, $audit_desc) = array_values($params);

        $date = date('Y-m-d H:i:s');
        $results = [
            'success' => 0,
            'failed' => 0,
            'errors' => '',
        ];

        if(count($ids) == 1){
            self::auditRefundOne($params, $admin);
            return $results;
        }

        $db = Util::db();

        $orderNodeList = $db->table('wa_v1_order_node')
            ->where('deleted', CommonConstant::DELETE_0)
            ->whereIn('id', $ids)
            ->select(self::node_field())
            ->get();
        if ($orderNodeList->isEmpty()) {
            throw new BusinessException('没有订单节点！');
        }

        if (count($orderNodeList) != count($ids)) {
            throw new BusinessException('部分订单节点不存在！');
        }

        // 提前检查节点状态
        $sourceNodes = $orderNodeList->where('source', CommonConstant::DEFAULT_1);
        if ($sourceNodes->isNotEmpty()) {
            throw new BusinessException('存在转户节点，不能退款');
        }

        // 提前检查节点状态
        $expiredNodes = $orderNodeList->where('status', CommonConstant::DEFAULT_1);
        if ($expiredNodes->isNotEmpty()) {
            throw new BusinessException('存在到期节点，不能退款');
        }

        // 提前检查节点状态
        $expiredNodes = $orderNodeList->where('status', CommonConstant::DEFAULT_2);
        if ($expiredNodes->isNotEmpty()) {
            throw new BusinessException('存在过期节点，不能退款');
        }

        // 提前检查节点状态
        $refundNodes = $orderNodeList->where('refund_status', CommonConstant::DEFAULT_2);
        if ($refundNodes->isNotEmpty()) {
            throw new BusinessException('存在退款审核通过节点，不能退款');
        }

        // 按用户分组处理，减少数据库操作
        $userNodes = [];
        foreach ($orderNodeList as $node) {
            $userNodes[$node->user_id][] = $node;
        }

        foreach ($userNodes as $userId => $nodes) {
            try {
                $db->transaction(function () use ($db, $nodes, $status, $audit_desc, $date, $admin, &$results) {
                    foreach ($nodes as $info) {
                        try {
                            // 订单节点退款记录ID
                            $refundLogId = $db->table('wa_v1_order_node_refund_log')
                                ->where('order_node_id', $info->id)
                                ->orderBy('id', 'desc')
                                ->value('id');

                            $data = [];
                            $data['refund_status'] = $status;
                            $data['refund_desc'] = $audit_desc;
                            $data['refund_time'] = $date;

                            if ($status == CommonConstant::DEFAULT_3) {
                                // 驳回退款
                                $db->table('wa_v1_order_node_refund_log')->where('id', $refundLogId)->update($data);
                                $db->table('wa_v1_order_node')->where('id', $info->id)->update($data);
                                $results['success']++;
                            } else {
                                // 通过退款
                                $user = $db->table('wa_admins')
                                    ->where('id', $info->user_id)
                                    ->select(['id', 'username'])
                                    ->first();
                                if (!$user) {
                                    throw new BusinessException("会员信息不存在或已删除！");
                                }

                                // 上级佣金数据（批量查询优化）
                                // 修复：查询该节点对应的实际佣金金额
                                $userCommissionLogItems = $db->table('wa_v1_user_commission_log_item')
                                    ->join('wa_v1_user_commission_log', 'wa_v1_user_commission_log.id', '=', 'wa_v1_user_commission_log_item.log_id')
                                    ->where('wa_v1_user_commission_log.admin_id', $user->id)
                                    ->whereIn('wa_v1_user_commission_log.type', [CommonConstant::MONEY_LOG_TYPE_4, CommonConstant::MONEY_LOG_TYPE_5])
                                    ->where('wa_v1_user_commission_log_item.item_id', $info->id)
                                    ->select('wa_v1_user_commission_log.id','wa_v1_user_commission_log.user_id', 'wa_v1_user_commission_log.user_name','wa_v1_user_commission_log.duration', 'wa_v1_user_commission_log_item.money')
                                    ->get();

                                $insertLogData = [];
                                foreach ($userCommissionLogItems as $userCommission) {
                                    $logData = [
                                        'admin_id' => $user->id,
                                        'admin_name' => $user->username,
                                        'user_id' => $userCommission->user_id,
                                        'user_name' => $userCommission->user_name,
                                        'type' => CommonConstant::MONEY_LOG_TYPE_10,
                                        'change_type' => CommonConstant::DEFAULT_20,
                                        'money' => $userCommission->money,
                                        'memo' => '节点退款【' . CommonConstant::get_duration_type_list()[$userCommission->duration] . '】',
                                        'order_node_ids' => $info->id,
                                    ];
                                    $insertLogData[] = $logData;
                                }

                                $logData = [
                                    'admin_id' => $admin['id'],
                                    'admin_name' => $admin['username'],
                                    'user_id' => $user->id,
                                    'user_name' => $user->username,
                                    'type' => CommonConstant::MONEY_LOG_TYPE_10,
                                    'change_type' => CommonConstant::DEFAULT_10,
                                    'money' => $info->amount,
                                    'memo' => '节点退款',
                                    'order_node_ids' => $info->id,
                                ];

                                // 更新订单节点退款记录
                                $db->table('wa_v1_order_node_refund_log')->where('id', $refundLogId)->update($data);

                                // 更新订单节点状态
                                $updateData = $data;
                                $updateData['expire_time'] = $date; // 退款通过 更新过期时间 走定时任务订单过期
                                $db->table('wa_v1_order_node')->where('id', $info->id)->update($updateData);

                                // 余额变动
                                UserService::money($logData, $logData['user_id'], []);

                                // 佣金变动
                                foreach ($insertLogData as $commissionLogData) {
                                    UserService::commission($commissionLogData, $commissionLogData['user_id'], [], ['type' => 'commission_refund']);
                                }

                                $results['success']++;
                            }
                        } catch (\Exception $e) {
                            $results['failed']++;
                            $results['errors'] .= "节点 {$info->id} 处理失败: " . $e->getMessage().' ';
                            // 继续处理下一个节点，不抛出异常
                        }
                    }
                });
            } catch (\Exception $e) {
                $results['failed'] += count($nodes);
                $results['errors'] .= "用户 {$userId} 的节点处理失败: " . $e->getMessage().' ';
            }
        }

        return $results;
    }
    public static function auditRefundOne($params, $admin)
    {
        list($id, $status, $audit_desc) = array_values($params);

        $db = Util::db();

        // 订单节点数据
        $info = $db->table('wa_v1_order_node')
            ->where('deleted', CommonConstant::DELETE_0)
            ->where('id', $id)
            ->select(self::node_field())
            ->first();
        if (!$info) {
            throw new BusinessException('没有订单节点！');
        }
        if ($info->status == CommonConstant::DEFAULT_1) {
            throw new BusinessException('存在到期节点，不能退款');
        }
        if ($info->status == CommonConstant::DEFAULT_2) {
            throw new BusinessException('存在过期节点，不能退款');
        }
        if ($info->refund_status == CommonConstant::DEFAULT_2) {
            throw new BusinessException('存在退款审核通过节点，不能退款');
        }

        // 订单节点退款记录ID
        $refundLogId = $db->table('wa_v1_order_node_refund_log')
            ->where('order_node_id', $id)
            ->orderBy('id', 'desc')
            ->value('id');

        $date = date('Y-m-d H:i:s');
        $data = [];
        $data['refund_status'] = $status;
        $data['refund_desc'] = $audit_desc;
        $data['refund_time'] = $date;
        if ($status == CommonConstant::DEFAULT_3) {
            $db->transaction(function () use ($db, $id, $refundLogId, $data) {
                // 更新订单节点退款记录
                $db->table('wa_v1_order_node_refund_log')->where('id', $refundLogId)->update($data);
                // 更新订单节点状态
                $db->table('wa_v1_order_node')->where('id', $id)->update($data);
            });
        } else {
            $user = $db->table('wa_admins')
                ->where('id', $info->user_id)
                ->select(['id', 'username'])
                ->first();
            if (!$user) {
                throw new BusinessException('会员信息不存在或已删除！');
            }

            // 上级佣金数据
            $userCommissionLogItems = $db->table('wa_v1_user_commission_log_item')
                ->join('wa_v1_user_commission_log', 'wa_v1_user_commission_log.id', '=', 'wa_v1_user_commission_log_item.log_id')
                ->where('wa_v1_user_commission_log.admin_id', $user->id)
                ->whereIn('wa_v1_user_commission_log.type', [CommonConstant::MONEY_LOG_TYPE_4, CommonConstant::MONEY_LOG_TYPE_5])
                ->where('wa_v1_user_commission_log_item.item_id', $id)
                ->select('wa_v1_user_commission_log.id','wa_v1_user_commission_log.user_id', 'wa_v1_user_commission_log.user_name','wa_v1_user_commission_log.duration', 'wa_v1_user_commission_log_item.money')
                ->get();

            $insertLogData = [];
            foreach ($userCommissionLogItems as $userCommission) {
                $logData = [
                    'admin_id' => $user->id,
                    'admin_name' => $user->username,
                    'user_id' => $userCommission->user_id,
                    'user_name' => $userCommission->user_name,
                    'type' => CommonConstant::MONEY_LOG_TYPE_10,
                    'change_type' => CommonConstant::DEFAULT_20,
                    'money' => $userCommission->money,
                    'memo' => '节点退款【' . CommonConstant::get_duration_type_list()[$userCommission->duration] . '】',
                    'order_node_ids' => $info->id,
                ];
                $insertLogData[] = $logData;
            }

            $logData = [
                'admin_id' => $admin['id'],
                'admin_name' => $admin['username'],
                'user_id' => $user->id,
                'user_name' => $user->username,
                'type' => CommonConstant::MONEY_LOG_TYPE_10,
                'change_type' => CommonConstant::DEFAULT_10,
                'money' => $info->amount,
                'memo' => '节点退款',
                'order_node_ids' => $info->id,
            ];
            $db->transaction(function () use ($db, $id, $refundLogId, $data, $logData, $insertLogData) {
                // 更新订单节点退款记录
                $db->table('wa_v1_order_node_refund_log')->where('id', $refundLogId)->update($data);
                // 更新订单节点状态
                $data['expire_time'] = $data['refund_time']; // 退款通过 更新过期时间 走定时任务订单过期
                $db->table('wa_v1_order_node')->where('id', $id)->update($data);

                // 余额变动
                UserService::money($logData, $logData['user_id'], []);

                foreach ($insertLogData as $logData) {
                    // 佣金变动
                    UserService::commission($logData, $logData['user_id'], [], ['type' => 'commission_refund']);
                }

            });
        }
        return true;
    }


    /**
     * 导入匹配
     *
     * @param array $params
     * @param array $admin
     * @param array $user
     */
    public static function importMatchingData($params, $admin, $user)
    {
        list($lineId, $isExpire, $content, $data) = array_values($params);

        $successData = [];
        $errorData = [];
        foreach ($data as $item) {
            $where = $item;
            unset($where['port'], $where['pass'], $where['expire_time']);
            $where['deleted'] = CommonConstant::DELETE_0;
            $where['user_id'] = $user['id'];
            $where['line_id'] = $lineId;
            $orderNodeInfo = V1OrderNode::where($where)
                ->with([
                    'game' => function ($query) {
                        $query->select(['id', 'name']);
                    },
                    'region' => function ($query) {
                        $query->select(['id', 'name']);
                    },
                    'server' => function ($query) {
                        $query->where('deleted', CommonConstant::DELETE_0)
                            ->select(['id', 'multi_port_status', 'multi_port']);
                    }
                ])
                ->first();
            if (!$orderNodeInfo) {
                $error = '|不存在';
                $errorData[] = implode('|', $item) . $error;
            } else {
                Util::getSk5Port($orderNodeInfo);
                if ($isExpire == CommonConstant::DEFAULT_0) {
                    $successData[] = $orderNodeInfo;
                }
                if ($isExpire == CommonConstant::DEFAULT_1 && $orderNodeInfo->status > CommonConstant::DEFAULT_0) {
                    $successData[] = $orderNodeInfo;
                }
            }
        }
        return compact('successData', 'errorData');
    }


    /**
     * API提取-订单列表
     *
     * @param string $page
     * @param string $limit
     * @param array $admin
     **/
    public static function getOrderList($page, $limit, $admin)
    {
        if ($admin['user_status'] == CommonConstant::DEFAULT_2) {
            $where['user_id'] = $admin['id'];
        }
        $where['deleted'] = CommonConstant::DELETE_0;
        $where['status'] = CommonConstant::DEFAULT_0;
        $db = Util::db();
        $query = $db->table('wa_v1_order')
            ->where($where)
            ->select(['id', 'order_no as name'])
            ->orderBy('id', 'desc');
        $paginator = $query->paginate($limit);
        return ['code' => 0, 'msg' => 'ok', 'data' => $paginator];
    }

    public static function getOrderListOld($page, $limit, $admin)
    {
        $limit = $limit > 0 ? $limit : 10;
        if (!($page >= 1)) $page = 1;
        $offset = ($page - 1) * $limit;

        if ($admin['user_status'] == CommonConstant::DEFAULT_2) {
            $where['user_id'] = $admin['id'];
        }
        $where['deleted'] = CommonConstant::DELETE_0;
        $db = Util::db();
        $total = $db->table('wa_v1_order')->where($where)->count('id');
        if (!$total) {
            return ['code' => 0, 'msg' => 'ok', 'count' => $total, 'data' => []];
        }
        $list = $query = $db->table('wa_v1_order')
            ->where($where)
            ->select(['id', 'order_no'])
            ->orderBy('id', 'desc')
            ->offset($offset)
            ->limit($limit)
            ->get();
        return ['code' => 0, 'msg' => 'ok', 'count' => $total, 'data' => $list];
    }

    /**
     * API提取-生成
     *
     * @param array $params
     * @param string $host
     * @param array $admin
     */
    public static function createApi($params, $host, $admin)
    {
        list($orderIds, $format, $line, $customLine, $output, $expireTime) = array_values($params);

        if ($admin['user_status'] == CommonConstant::DEFAULT_2) {
            $where['user_id'] = $admin['id'];
        }
        $where['deleted'] = CommonConstant::DELETE_0;
        $db = Util::db();
        $orderIds = $db->table('wa_v1_order')
            ->where($where)
            ->whereIn('id', $orderIds)
            ->pluck('id')
            ->toArray();
        if (!$orderIds) {
            throw new BusinessException('订单不存在或已删除！');
        }

        $token = Random::uuid();
        $expire_time = $expireTime > 0 ? $expireTime = date('Y-m-d H:i:s', ($expireTime * 86400)) : null;
        $data = [];
        $data['token'] = $token;
        $data['order_ids'] = implode(',', $orderIds);
        $data['format'] = $format;
        $data['line'] = $line;
        $data['custom_line'] = $customLine;
        $data['output'] = $output;
        $data['expire_time'] = $expire_time;
        $db->transaction(function () use ($db, $data) {
            $db->table('wa_v1_order_token')->insert($data);
        });

        $api_url = $host . '/index/getOrderByToken?token=' . $token;
        return compact('api_url');
    }

    /**
     * API提取-提取
     *
     * @param array $params
     **/
    public static function extractApi($params)
    {
        list($token) = array_values($params);

        $db = Util::db();
        $info = $db->table('wa_v1_order_token')->where('token', $token)->first();
        if (!$info) {
            throw new BusinessException('token已失效！');
        }
        if ($info->expire_time) {
            if (strtotime($info->expire_time) <= time()) {
                throw new BusinessException('token已过期！');
            }
        }
        $orderIds = explode(',', $info->order_ids);
        $orderNodeList = $db->table('wa_v1_order_node')
            ->where('deleted', CommonConstant::DELETE_0)
            ->where('status', CommonConstant::DEFAULT_0)
            ->whereIn('refund_status', [CommonConstant::DEFAULT_0, CommonConstant::DEFAULT_3])
            ->whereIn('order_id', $orderIds)
            ->select(['id', 'server_id', 'domain', 'ip', 'port', 'account', 'pass', 'expire_time'])
            ->with([
                'server' => function ($query) {
                    $query->where('deleted', CommonConstant::DELETE_0)
                        ->select(['id', 'multi_port_status', 'multi_port']);
                }
            ])
            ->get();
        $txt = '';
        $data = [];
        if ($orderNodeList->isEmpty()) {
            return json(['code' => 0, 'msg' => '没有数据', 'data' => $data]);
        }
        $items = $orderNodeList;
        foreach ($items as $item) {
            Util::getSk5Port($item);

            if ($info->output == CommonConstant::DEFAULT_1) {
                $ip = $item->ip;
            } else {
                $ip = $item->domain;
            }
            switch ($info->line) {
                case 1:
                    $line_txt = "\r\n";
                    break;
                case 2:
                    $line_txt = "\n";
                    break;
                case 3:
                    $line_txt = " ";
                    break;
                case 4:
                    $line_txt = $info->custom_line;
                    break;
            }
            $content = $ip . $line_txt . $item->port . $line_txt . $item->account . $line_txt . $item->pass . $line_txt . $item->expire_time;
            if ($info->format == CommonConstant::DEFAULT_1) {
                $txt .= ' ' . $content;
            } else {
                $data[] = $content;
            }
        }
        if ($info->format == CommonConstant::DEFAULT_1) {
            return $txt;
        } else {
            return json(['code' => 0, 'msg' => 'ok', 'data' => $data]);
        }
    }


    // 定时任务

    /**
     * 订单到期
     **/
    public static function orderExpire()
    {
        $date = date('Y-m-d H:i:s');

        $db = Util::db();
        $orderIds = $db->table('wa_v1_order')
            ->where('deleted', CommonConstant::DELETE_0)
            ->where('status', CommonConstant::DEFAULT_0)
            ->where('expire_time', '<=', $date)
            ->orderBy('id', 'desc')
            ->limit(1000)
            ->pluck('id')
            ->toArray();
        $orderNodeList = $db->table('wa_v1_order_node')
            ->where('deleted', CommonConstant::DELETE_0)
            ->where('status', CommonConstant::DEFAULT_0)
            ->whereIn('refund_status', [CommonConstant::DEFAULT_0, CommonConstant::DEFAULT_1, CommonConstant::DEFAULT_3])
            ->where('expire_time', '<=', $date)
            ->select(['id', 'duration', 'account', 'data'])
            ->limit(1000)
            ->get();

        $orderNodeIds = $orderNodeList->pluck('id')->toArray();
        $db->transaction(function () use ($db, $orderIds, $orderNodeIds) {
            $data['status'] = CommonConstant::DEFAULT_1;
            if ($orderIds) {
                $db->table('wa_v1_order')
                    ->whereIn('id', $orderIds)
                    ->update($data);
            }
            if ($orderNodeIds) {
                $data['renew_status'] = CommonConstant::DEFAULT_0;
                $data['refund_status'] = CommonConstant::DEFAULT_0;
                $db->table('wa_v1_order_node')
                    ->whereIn('id', $orderNodeIds)
                    ->update($data);
            }
        });
        if ($orderNodeIds) {
            foreach ($orderNodeList as $orderNode) {
                $isTestDuration = in_array($orderNode->duration, [
                    CommonConstant::TIME_0,
                    CommonConstant::TIME_1,
                    CommonConstant::TIME_2
                ]);
                if (!$isTestDuration) {
                    $array = explode('|', $orderNode->data);
                    $url = $array[0];
                    $apiData = [];
                    $apiData['key'] = $array[1];
                    $apiData['username'] = $orderNode->account;
                    Util::userApi('del', $url, $apiData);
                }
            }
        }
        return true;
    }

    /**
     * 订单节点保留到期
     **/
    public static function orderReserveExpire()
    {
        $hourAgo = OptionService::getConfigReserve();
        $date = date('Y-m-d H:i:s', $hourAgo);

        $db = Util::db();
        $orderNodeIds = $db->table('wa_v1_order_node')
            ->where('deleted', CommonConstant::DELETE_0)
            ->where('status', CommonConstant::DEFAULT_1)
            ->where('expire_time', '<=', $date)
            ->select(['id'])
            ->orderBy('id', 'desc')
            ->limit(1000)
            ->pluck('id')
            ->toArray();
        if (!$orderNodeIds) {
            throw new BusinessException('没有订单节点！');
        }

        $db->transaction(function () use ($db, $orderNodeIds) {
            $data['status'] = CommonConstant::DEFAULT_2;
            $data['renew_status'] = CommonConstant::DEFAULT_0;
            $db->table('wa_v1_order_node')
                ->whereIn('id', $orderNodeIds)
                ->update($data);
        });
        return true;
    }

    /**
     * 获取自动续费列表
     **/
    public static function renewAuto()
    {
        $config = OptionService::getConfig('order');
        $seconds = $config['order_node_renew_time'] * 86400;
        $time = time();
        if (!$seconds) {
            throw new BusinessException('节点到期前几天自动续费配置错误！');
        }
        if (!$config['order_node_renew_duration']) {
            throw new BusinessException('续费时间配置错误！');
        }

        $db = Util::db();
        $orderNodeList = $db->table('wa_v1_order_node')
            ->where('deleted', CommonConstant::DELETE_0)
            ->where('status', CommonConstant::DEFAULT_0)
            ->whereIn('refund_status', [CommonConstant::DEFAULT_0, CommonConstant::DEFAULT_3])
            ->where('renew_status', CommonConstant::DEFAULT_1)
            ->select(self::node_field())
            ->orderBy('id', 'desc')
            ->get();
        if ($orderNodeList->isEmpty()) {
            throw new BusinessException('没有开启自动续费订单节点！');
        }

        $orderNodeIds = [];
        foreach ($orderNodeList as $orderNode) {
            $ago = strtotime($orderNode->expire_time) - $seconds;
            if ($time >= $ago) {
                $orderNodeId = $orderNode->id;
                $orderNodeIds[] = $orderNode->id;

                // 队列名
                // 数据，可以直接传数组，无需序列化
                // 投递消息
                Redis::send('renew_auto', ['id' => $orderNodeId, 'data' => $orderNode], 1);
            }
        }
        if (!$orderNodeIds) {
            throw new BusinessException('没有自动续费订单节点！');
        }
        return true;
    }
}
